build member tables of instantiated classes/interfaces lazily - #64475
Max Schwenk (maschwenk) wants to merge 10 commits into
Conversation
Looking up one property of an instantiated class or interface reference (`expect(x).toBe`, `schema.optional`, `arr.map`) resolved all of its members: every declared member was instantiated and every inherited member merged, although most of those symbols are never used. The first lookup on such a reference now prepares a lazy member table instead. Preparing does everything resolveObjectTypeMembers does except create the member symbols, in the same order, so everything that is resolved along the way (signatures, index infos, base types and their members) is resolved exactly as before. It also records which declared members instantiateSymbol would return as they are at that point, since that depends on what has been resolved. Lookups then instantiate only the requested member, walking the prepared base types in addInheritedMembers order, and resolving the members in full later reuses the symbols already handed out. If preparing leads back to the reference, it exposes the same partial members resolveObjectTypeMembers would, and is resolved in full from then on. A lookup of a missing name answers the Object/Function augmentation from the number of call and construct signatures, counted from the declared signatures and those of the prepared base types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bers isWeakType, getSingleSignature, getSignaturesOfStructuredType, getIndexInfosOfStructuredType, isEmptyObjectType, isFunctionObjectType and isStringIndexSignatureOnlyType resolved all members of an instantiated class or interface reference just to count its properties, signatures or index infos, or to see whether its properties are optional. For a reference with a lazy member table these are now answered from the table. Signatures are counted from the instantiated declared signatures and those of the prepared base types. Whether there are properties, and whether all of them are optional, follows from the declared members (which have the same flags as their instantiations) and the properties of the base types, merged as in addInheritedMembers. Index infos are merged from the instantiated declared index infos and those of the base types, as in resolveObjectTypeMembers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolving the members of a lazily prepared reference in full stored every instantiated member in the table's memo, which is discarded right after, and checked each member against the list of members that instantiate to themselves with a linear scan. The list is now sorted and binary searched, and members are only memoized when looked up individually. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Property and index info lookups are hot, and nearly always see types whose members are resolved. They now check that inline before calling into the lazy member table code, which on material-ui's docs project took 2-3% of check time on its own. The lazy part of getPropertyOfTypeEx moves to getPropertyOfObjectTypeLazily. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each shape check (isWeakType, getSingleSignature, isStringIndexSignatureOnlyType, isFunctionObjectType, getPropertyOfTypeEx) had a second, lazy version of its condition. Now the lazy table stores the reference's full signatures and index infos, inherited through a helper resolveObjectTypeMembers shares, and getSignaturesOfStructuredType and getIndexInfosOfStructuredType answer from it. The checks read through those accessors and getMemberOfStructuredType, hasPropertiesOfStructuredType and everyPropertyOfStructuredType, so each condition is written once. The separate shape summary goes away, as do the hooks in getPropertyOfObjectType and isEmptyObjectType, which saved no memory, and the code moves into checker.go. This also removes a crash path: isWeakType read the table's shape, called getIndexInfosOfStructuredType, and read the shape again, which would find no table if that call had resolved the type in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Max Schwenk (@maschwenk) I definitely want to land #64499, really nice savings from that one. Once it is in, let's see how much extra savings we can get from this one. I'm a bit concerned with adding ~500 lines of code to member resolution. |
resolveObjectTypeMembers no longer exposes a type's declared members while its base types resolve, so the lazy table doesn't need to either. A table is now either being prepared, during which the type resolves its members as usual, or ready; the materialized state, the count of inherited base types and the partial lookup paths go away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inherited properties are looked up with getPropertyOfTypeEx instead of a separate helper, lookups are no longer memoized (the memo cost more memory than it saved, with no change in check time), and a few one-use helpers are inlined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Anders Hejlsberg (@ahejlsberg) thanks for landing this! merged main in, and with this one in i could drop all the half built member handling and trim the rest: so the code is +254/−37 now (was ~570), plus one test and its generated baselines. on top of current main:
mui-docs is about flat on memory and ~2% slower. full table is in the description also noticed the new never-reduction check walks the |
One test covering what the two did that the existing suite doesn't: member lookups with redeclared and private members, base types whose shape depends on the instantiation (weak types, bind, string index signatures, merged signatures), an interface extending a type parameter, a recursive base type and function narrowing. Baselines are generated on unmodified main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Max Schwenk (@maschwenk) Thanks for doing the research here. Your PR definitely demonstrates there are potential wins from selectively resolving individual object type members, but I would much prefer to chew on it a bit and think about an implementation that combines the disparate code paths. Ideally, full resolution would just consist of resolving all individual members through a single code path. |
|
Anders Hejlsberg (@ahejlsberg) totally understand! thank you! was just making sure it didn't drop off notifications ❤️ |
Part of this issue (the intersection half landed separately, see related PRs at the bottom):
looking up one property on an instantiated class or interface builds the whole member table right now. every declared member gets instantiated and every inherited one gets merged in, even though most of them never get used. a few shape checks (
isWeakType,getSingleSignature,isStringIndexSignatureOnlyType,isFunctionObjectType) do the same thing just to count stuffhistory: the first version (up to af2719b) had a separate lazy copy of each shape check, cdac4e3 reworked it to go through the accessors, after merging main 39ce30c drops the half built member handling that the idempotency change made unnecessary, d40b4bd trims the lookup (no memo, reuses
getPropertyOfTypeExfor bases), and the last two commits cut the tests down to one and trim comments. the code is +254/−37 now (was ~570), the rest is one 74 line test and its generated baselinesthis PR vs main at 0681ef7 (so with both related PRs below), typescript-benchmarking, median of 3:
on our 37k-file program, also on 0681ef7: heap 19.1 → 15.3 GiB with 4 checkers (−20%) and 12.6 → 10.5 GiB single threaded (−17%), check time −6% single threaded. same diagnostics on every run. raw results and scripts: https://gist.github.com/maschwenk/c83d9185c6961b6fb43d7d071ef39cf6
full go suite passes (also multiple checkers and
-race), lint and format are clean. addedinstantiatedReferenceLazyMembers.tswith baselines generated on unmodified main, so it pins that the output didnt changerelated:
resolveObjectTypeMembers#64372used claude code to help write this, ive reviewed it